package minecraft

import (
	"bytes"
	"context"
	"crypto/ecdsa"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"fmt"
	"io"
	"log"
	"net"
	"strings"
	"sync"
	"time"

	"github.com/google/uuid"
	"github.com/sandertv/go-raknet"
	"go.uber.org/atomic"
	"gopkg.in/square/go-jose.v2"
	"gopkg.in/square/go-jose.v2/jwt"
	"main.go/minecraft/internal"
	"main.go/minecraft/protocol"
	"main.go/minecraft/protocol/login"
	"main.go/minecraft/protocol/packet"
	"main.go/minecraft/resource"
	"main.go/minecraft/text"
)

// exemptedResourcePack is a resource pack that is exempted from being downloaded. These packs may be directly
// applied by sending them in the ResourcePackStack packet.
type exemptedResourcePack struct {
	uuid    string
	version string
}

// exemptedPacks is a list of all resource packs that do not need to be downloaded, but may always be applied
// in the ResourcePackStack packet.
var exemptedPacks = []exemptedResourcePack{
	{
		uuid:    "6baf8b62-8948-4c99-bb1e-a0cb35dc4579",
		version: "1.0.0",
	},
	{
		uuid:    "0fba4063-dba1-4281-9b89-ff9390653530",
		version: "1.0.0",
	},
}

// Conn represents a Minecraft (Bedrock Edition) connection over a specific net.Conn transport layer. Its
// methods (Read, Write etc.) are safe to be called from multiple goroutines simultaneously, but ReadPacket
// must not be called on multiple goroutines simultaneously.
type Conn struct {
	// once is used to ensure the Conn is closed only a single time. It protects the channel below from being
	// closed multiple times.
	once  sync.Once
	close chan struct{}

	conn        net.Conn
	log         *log.Logger
	authEnabled bool

	pool packet.Pool
	enc  *packet.Encoder
	dec  *packet.Decoder

	identityData login.IdentityData
	clientData   login.ClientData

	gameData         GameData
	gameDataReceived atomic.Bool
	chunkRadius      int

	// privateKey is the private key of this end of the connection. Each connection, regardless of which side
	// the connection is on, server or client, has a unique private key generated.
	privateKey *ecdsa.PrivateKey
	// salt is a 16 byte long randomly generated byte slice which is only used if the Conn is a server sided
	// connection. It is otherwise left unused.
	salt []byte

	// packets is a channel of byte slices containing serialised packets that are coming in from the other
	// side of the connection.
	packets chan *packetData

	deferredPacketMu sync.Mutex
	// deferredPackets is a list of packets that were pushed back during the login sequence because they
	// were not used by the connection yet. These packets are read the first when calling to Read or
	// ReadPacket after being connected.
	deferredPackets []*packetData
	readDeadline    <-chan time.Time

	sendMu sync.Mutex
	// bufferedSend is a slice of byte slices containing packets that are 'written'. They are buffered until
	// they are sent each 20th of a second.
	bufferedSend [][]byte
	hdr          *packet.Header

	// loggedIn is a bool indicating if the connection was logged in. It is set to true after the entire login
	// sequence is completed.
	loggedIn bool
	// spawn is a bool channel indicating if the connection is currently waiting for its spawning in
	// the world: It is completing a sequence that will result in the spawning.
	spawn           chan struct{}
	waitingForSpawn atomic.Bool

	// expectedIDs is a slice of packet identifiers that are next expected to arrive, until the connection is
	// logged in.
	expectedIDs atomic.Value

	packMu sync.Mutex
	// resourcePacks is a slice of resource packs that the listener may hold. Each client will be asked to
	// download these resource packs upon joining.
	resourcePacks []*resource.Pack
	// texturePacksRequired specifies if clients that join must accept the texture pack in order for them to
	// be able to join the server. If they don't accept, they can only leave the server.
	texturePacksRequired bool
	packQueue            *resourcePackQueue

	cacheEnabled bool

	// packetFunc is an optional function passed to a Dial() call. If set, each packet read from and written
	// to this connection will call this function.
	packetFunc func(header packet.Header, payload []byte, src, dst net.Addr)

	disconnectMessage atomic.String

	shieldID  atomic.Int32
	DebugMode bool
}

// newConn creates a new Minecraft connection for the net.Conn passed, reading and writing compressed
// Minecraft packets to that net.Conn.
// newConn accepts a private key which will be used to identify the connection. If a nil key is passed, the
// key is generated.
func newConn(netConn net.Conn, key *ecdsa.PrivateKey, log *log.Logger) *Conn {
	conn := &Conn{
		enc:         packet.NewEncoder(netConn),
		dec:         packet.NewDecoder(netConn),
		pool:        packet.NewPool(),
		salt:        make([]byte, 16),
		packets:     make(chan *packetData, 8),
		close:       make(chan struct{}),
		spawn:       make(chan struct{}),
		conn:        netConn,
		privateKey:  key,
		log:         log,
		chunkRadius: 16,
		hdr:         &packet.Header{},
	}
	conn.expectedIDs.Store([]uint32{packet.IDLogin})
	_, _ = rand.Read(conn.salt)

	go func() {
		ticker := time.NewTicker(time.Second / 20)
		defer ticker.Stop()
		for range ticker.C {
			if err := conn.Flush(); err != nil {
				_ = conn.Close()
				return
			}
		}
	}()
	return conn
}

// IdentityData returns the identity data of the connection. It holds the UUID, XUID and username of the
// connected client.
func (conn *Conn) IdentityData() login.IdentityData {
	return conn.identityData
}

// ClientData returns the client data the client connected with. Note that this client data may be changed
// during the session, so the data should only be used directly after connection, and should be updated after
// that by the caller.
func (conn *Conn) ClientData() login.ClientData {
	return conn.clientData
}

// Authenticated returns true if the connection was authenticated through XBOX Live services.
func (conn *Conn) Authenticated() bool {
	return conn.IdentityData().XUID != ""
}

// GameData returns specific game data set to the connection for the player to be initialised with. If the
// Conn is obtained using Listen, this game data may be set to the Listener. If obtained using Dial, the data
// is obtained from the server.
func (conn *Conn) GameData() GameData {
	return conn.gameData
}

// StartGame starts the game for a client that connected to the server. StartGame should be called for a Conn
// obtained using a minecraft.Listener. The game data passed will be used to spawn the player in the world of
// the server. To spawn a Conn obtained from a call to minecraft.Dial(), use Conn.DoSpawn().
func (conn *Conn) StartGame(data GameData) error {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
	defer cancel()
	return conn.StartGameContext(ctx, data)
}

// StartGameTimeout starts the game for a client that connected to the server, returning an error if the
// connection is not yet fully connected while the timeout expires.
// StartGameTimeout should be called for a Conn obtained using a minecraft.Listener. The game data passed will
// be used to spawn the player in the world of the server. To spawn a Conn obtained from a call to
// minecraft.Dial(), use Conn.DoSpawn().
func (conn *Conn) StartGameTimeout(data GameData, timeout time.Duration) error {
	ctx, cancel := context.WithTimeout(context.Background(), timeout)
	defer cancel()
	return conn.StartGameContext(ctx, data)
}

// StartGameContext starts the game for a client that connected to the server, returning an error if the
// context is closed while spawning the client.
// StartGameContext should be called for a Conn obtained using a minecraft.Listener. The game data passed will
// be used to spawn the player in the world of the server. To spawn a Conn obtained from a call to
// minecraft.Dial(), use Conn.DoSpawn().
func (conn *Conn) StartGameContext(ctx context.Context, data GameData) error {
	if conn.gameDataReceived.Load() {
		panic("(*Conn).StartGame must only be called on Listener connections")
	}
	if data.WorldName == "" {
		data.WorldName = conn.gameData.WorldName
	}

	conn.gameData = data
	for _, item := range data.Items {
		if item.Name == "minecraft:shield" {
			conn.shieldID.Store(int32(item.RuntimeID))
		}
	}
	conn.waitingForSpawn.Store(true)
	conn.startGame()

	select {
	case <-conn.close:
		return conn.closeErr("start game")
	case <-ctx.Done():
		return conn.wrap(ctx.Err(), "start game")
	case <-conn.spawn:
		// Conn was spawned successfully.
		return nil
	}
}

// DoSpawn starts the game for the client in the server. DoSpawn should be called for a Conn obtained using
// minecraft.Dial(). Use Conn.StartGame to spawn a Conn obtained using a minecraft.Listener.
// DoSpawn will start the spawning sequence using the game data found in conn.GameData(), which was sent
// earlier by the server.
// DoSpawn has a default timeout of 30 seconds. DoSpawnContext or DoSpawnTimeout may be used for cancellation
// at any other times.
func (conn *Conn) DoSpawn() error {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
	defer cancel()
	return conn.DoSpawnContext(ctx)
}

// DoSpawnTimeout starts the game for the client in the server with a timeout after which an error is
// returned if the client has not yet spawned by that time. DoSpawnTimeout should be called for a Conn
// obtained using minecraft.Dial(). Use Conn.StartGame to spawn a Conn obtained using a minecraft.Listener.
// DoSpawnTimeout will start the spawning sequence using the game data found in conn.GameData(), which was
// sent earlier by the server.
func (conn *Conn) DoSpawnTimeout(timeout time.Duration) error {
	ctx, cancel := context.WithTimeout(context.Background(), timeout)
	defer cancel()
	return conn.DoSpawnContext(ctx)
}

// DoSpawnContext starts the game for the client in the server, using a specific context for cancellation.
// DoSpawnContext should be called for a Conn obtained using minecraft.Dial(). Use Conn.StartGame to spawn a
// Conn obtained using a minecraft.Listener.
// DoSpawnContext will start the spawning sequence using the game data found in conn.GameData(), which was
// sent earlier by the server.
func (conn *Conn) DoSpawnContext(ctx context.Context) error {
	if !conn.gameDataReceived.Load() {
		panic("(*Conn).DoSpawn must only be called on Dialer connections")
	}
	conn.waitingForSpawn.Store(true)

	select {
	case <-conn.close:
		return conn.closeErr("do spawn")
	case <-ctx.Done():
		return conn.wrap(ctx.Err(), "do spawn")
	case <-conn.spawn:
		// Conn was spawned successfully.
		return nil
	}
}

// WritePacket encodes the packet passed and writes it to the Conn. The encoded data is buffered until the
// next 20th of a second, after which the data is flushed and sent over the connection.
func (conn *Conn) WritePacket(pk packet.Packet) error {
	if conn.DebugMode {
		return nil
	}
	select {
	case <-conn.close:
		return conn.closeErr("write packet")
	default:
	}
	conn.sendMu.Lock()
	defer conn.sendMu.Unlock()

	buf := internal.BufferPool.Get().(*bytes.Buffer)
	defer func() {
		// Reset the buffer so we can return it to the buffer pool safely.
		buf.Reset()
		internal.BufferPool.Put(buf)
	}()

	conn.hdr.PacketID = pk.ID()
	_ = conn.hdr.Write(buf)
	l := buf.Len()

	pk.Marshal(protocol.NewWriter(buf, conn.shieldID.Load()))
	if conn.packetFunc != nil {
		conn.packetFunc(*conn.hdr, buf.Bytes()[l:], conn.LocalAddr(), conn.RemoteAddr())
	}

	conn.bufferedSend = append(conn.bufferedSend, append([]byte(nil), buf.Bytes()...))
	return nil
}

// ReadPacket reads a packet from the Conn, depending on the packet ID that is found in front of the packet
// data. If a read deadline is set, an error is returned if the deadline is reached before any packet is
// received. ReadPacket must not be called on multiple goroutines simultaneously.
//
// If the packet read was not implemented, a *packet.Unknown is returned, containing the raw payload of the
// packet read.
func (conn *Conn) ReadPacket() (pk packet.Packet, err error) {
	if data, ok := conn.takeDeferredPacket(); ok {
		pk, err := data.decode(conn)
		if err != nil {
			conn.log.Println(err)
			return conn.ReadPacket()
		}
		return pk, nil
	}

	select {
	case <-conn.close:
		return nil, conn.closeErr("read packet")
	case <-conn.readDeadline:
		return nil, conn.wrap(context.DeadlineExceeded, "read packet")
	case data := <-conn.packets:
		pk, err := data.decode(conn)
		if err != nil {
			conn.log.Println(err)
			return conn.ReadPacket()
		}
		return pk, nil
	}
}

// ResourcePacks returns a slice of all resource packs the connection holds. For a Conn obtained using a
// Listener, this holds all resource packs set to the Listener. For a Conn obtained using Dial, the resource
// packs include all packs sent by the server connected to.
func (conn *Conn) ResourcePacks() []*resource.Pack {
	return conn.resourcePacks
}

// Write writes a slice of serialised packet data to the Conn. The data is buffered until the next 20th of a
// tick, after which it is flushed to the connection. Write returns the amount of bytes written n.
func (conn *Conn) Write(b []byte) (n int, err error) {
	conn.sendMu.Lock()
	defer conn.sendMu.Unlock()

	conn.bufferedSend = append(conn.bufferedSend, b)
	return len(b), nil
}

// Read reads a packet from the connection into the byte slice passed, provided the byte slice is big enough
// to carry the full packet.
// It is recommended to use ReadPacket() rather than Read() in cases where reading is done directly.
func (conn *Conn) Read(b []byte) (n int, err error) {
	if data, ok := conn.takeDeferredPacket(); ok {
		if len(b) < len(data.full) {
			return 0, conn.wrap(errBufferTooSmall, "read")
		}
		return copy(b, data.full), nil
	}
	select {
	case <-conn.close:
		return 0, conn.closeErr("read")
	case <-conn.readDeadline:
		return 0, conn.wrap(context.DeadlineExceeded, "read")
	case data := <-conn.packets:
		if len(b) < len(data.full) {
			return 0, conn.wrap(errBufferTooSmall, "read")
		}
		return copy(b, data.full), nil
	}
}

// Flush flushes the packets currently buffered by the connections to the underlying net.Conn, so that they
// are directly sent.
func (conn *Conn) Flush() error {
	select {
	case <-conn.close:
		return conn.closeErr("flush")
	default:
	}
	conn.sendMu.Lock()
	defer conn.sendMu.Unlock()

	if len(conn.bufferedSend) > 0 {
		if err := conn.enc.Encode(conn.bufferedSend); err != nil && !raknet.ErrConnectionClosed(err) {
			// Should never happen.
			panic(fmt.Errorf("error encoding packet batch: %v", err))
		}
		// First manually clear out conn.bufferedSend so that re-using the slice after resetting its length to
		// 0 doesn't result in an 'invisible' memory leak.
		for i := range conn.bufferedSend {
			conn.bufferedSend[i] = nil
		}
		// Slice the conn.bufferedSend to a length of 0 so we don't have to re-allocate space in this slice
		// every time.
		conn.bufferedSend = conn.bufferedSend[:0]
	}
	return nil
}

// Close closes the Conn and its underlying connection. Before closing, it also calls Flush() so that any
// packets currently pending are sent out.
func (conn *Conn) Close() error {
	if conn.DebugMode {
		return nil
	}
	var err error
	conn.once.Do(func() {
		close(conn.close)
		_ = conn.Flush()
		err = conn.conn.Close()
	})
	return err
}

// LocalAddr returns the local address of the underlying connection.
func (conn *Conn) LocalAddr() net.Addr {
	return conn.conn.LocalAddr()
}

// RemoteAddr returns the remote address of the underlying connection.
func (conn *Conn) RemoteAddr() net.Addr {
	return conn.conn.RemoteAddr()
}

// SetDeadline sets the read and write deadline of the connection. It is equivalent to calling SetReadDeadline
// and SetWriteDeadline at the same time.
func (conn *Conn) SetDeadline(t time.Time) error {
	return conn.SetReadDeadline(t)
}

// SetReadDeadline sets the read deadline of the Conn to the time passed. The time must be after time.Now().
// Passing an empty time.Time to the method (time.Time{}) results in the read deadline being cleared.
func (conn *Conn) SetReadDeadline(t time.Time) error {
	if t.Before(time.Now()) {
		panic(fmt.Errorf("error setting read deadline: time passed is before time.Now()"))
	}
	empty := time.Time{}
	if t == empty {
		conn.readDeadline = make(chan time.Time)
	} else {
		conn.readDeadline = time.After(time.Until(t))
	}
	return nil
}

// SetWriteDeadline is a stub function to implement net.Conn. It has no functionality.
func (conn *Conn) SetWriteDeadline(time.Time) error {
	return nil
}

// Latency returns a rolling average of latency between the sending and the receiving end of the connection.
// The latency returned is updated continuously and is half the round trip time (RTT).
func (conn *Conn) Latency() time.Duration {
	if c, ok := conn.conn.(interface {
		Latency() time.Duration
	}); ok {
		return c.Latency()
	}
	panic(fmt.Sprintf("connection type %T has no Latency() time.Duration method", conn.conn))
}

// ClientCacheEnabled checks if the connection has the client blob cache enabled. If true, the server may send
// blobs to the client to reduce network transmission, but if false, the client does not support it, and the
// server must send chunks as usual.
func (conn *Conn) ClientCacheEnabled() bool {
	return conn.cacheEnabled
}

// ChunkRadius returns the initial chunk radius of the connection. For connections obtained through a
// Listener, this is the radius that the client requested. For connections obtained through a Dialer, this
// is the radius that the server approved upon.
func (conn *Conn) ChunkRadius() int {
	return conn.chunkRadius
}

// takeDeferredPacket locks the deferred packets lock and takes the next packet from the list of deferred
// packets. If none was found, it returns false, and if one was found, the data and true is returned.
func (conn *Conn) takeDeferredPacket() (*packetData, bool) {
	conn.deferredPacketMu.Lock()
	defer conn.deferredPacketMu.Unlock()

	if len(conn.deferredPackets) == 0 {
		return nil, false
	}
	data := conn.deferredPackets[0]
	// Explicitly clear out the packet at offset 0. When we slice it to remove the first element, that element
	// will not be garbage collectable, because the array it's in is still referenced by the slice. Doing this
	// makes sure garbage collecting the packet is possible.
	conn.deferredPackets[0] = nil
	conn.deferredPackets = conn.deferredPackets[1:]
	return data, true
}

// deferPacket defers a packet so that it is obtained in the next ReadPacket call
func (conn *Conn) deferPacket(pk *packetData) {
	conn.deferredPacketMu.Lock()
	conn.deferredPackets = append(conn.deferredPackets, pk)
	conn.deferredPacketMu.Unlock()
}

// receive receives an incoming serialised packet from the underlying connection. If the connection is not yet
// logged in, the packet is immediately handled.
func (conn *Conn) receive(data []byte) error {
	pkData, err := parseData(data, conn)
	if err != nil {
		return err
	}
	if pkData.h.PacketID == packet.IDDisconnect {
		// We always handle disconnect packets and close the connection if one comes in.
		pk, _ := pkData.decode(conn)

		conn.disconnectMessage.Store(pk.(*packet.Disconnect).Message)
		_ = conn.Close()
		return nil
	}
	if conn.loggedIn && !conn.waitingForSpawn.Load() {
		select {
		case <-conn.close:
		case previous := <-conn.packets:
			// There was already a packet in this channel, so take it out and defer it so that it is read
			// next.
			conn.deferPacket(previous)
		default:
		}
		select {
		case <-conn.close:
		case conn.packets <- pkData:
		}
		return nil
	}
	return conn.handle(pkData)
}

// handle tries to handle the incoming packetData.
func (conn *Conn) handle(pkData *packetData) error {
	for _, id := range conn.expectedIDs.Load().([]uint32) {
		if id == pkData.h.PacketID {
			// If the packet was expected, so we handle it right now.
			pk, err := pkData.decode(conn)
			if err != nil {
				return err
			}
			return conn.handlePacket(pk)
		}
	}
	// This is not the packet we expected next in the login sequence. We push it back so that it may
	// be handled by the user.
	conn.deferPacket(pkData)
	return nil
}

// handlePacket handles an incoming packet. It returns an error if any of the data found in the packet was not
// valid or if handling failed for any other reason.
func (conn *Conn) handlePacket(pk packet.Packet) error {
	defer func() {
		_ = conn.Flush()
	}()
	switch pk := pk.(type) {
	// Internal packets destined for the server.
	case *packet.Login:
		return conn.handleLogin(pk)
	case *packet.ClientToServerHandshake:
		return conn.handleClientToServerHandshake()
	case *packet.ClientCacheStatus:
		return conn.handleClientCacheStatus(pk)
	case *packet.ResourcePackClientResponse:
		return conn.handleResourcePackClientResponse(pk)
	case *packet.ResourcePackChunkRequest:
		return conn.handleResourcePackChunkRequest(pk)
	case *packet.RequestChunkRadius:
		return conn.handleRequestChunkRadius(pk)
	case *packet.SetLocalPlayerAsInitialised:
		return conn.handleSetLocalPlayerAsInitialised(pk)

	// Internal packets destined for the client.
	case *packet.ServerToClientHandshake:
		return conn.handleServerToClientHandshake(pk)
	case *packet.PlayStatus:
		return conn.handlePlayStatus(pk)
	case *packet.ResourcePacksInfo:
		return conn.handleResourcePacksInfo(pk)
	case *packet.ResourcePackDataInfo:
		return conn.handleResourcePackDataInfo(pk)
	case *packet.ResourcePackChunkData:
		return conn.handleResourcePackChunkData(pk)
	case *packet.ResourcePackStack:
		return conn.handleResourcePackStack(pk)
	case *packet.StartGame:
		return conn.handleStartGame(pk)
	case *packet.ChunkRadiusUpdated:
		return conn.handleChunkRadiusUpdated(pk)
	}
	return nil
}

// handleLogin handles an incoming login packet. It verifies an decodes the login request found in the packet
// and returns an error if it couldn't be done successfully.
func (conn *Conn) handleLogin(pk *packet.Login) error {
	// The next expected packet is a response from the client to the handshake.
	conn.expect(packet.IDClientToServerHandshake)
	var (
		err        error
		authResult login.AuthResult
	)
	conn.identityData, conn.clientData, authResult, err = login.Parse(pk.ConnectionRequest)
	if err != nil {
		return fmt.Errorf("parse login request: %w", err)
	}

	// Make sure the player is logged in with XBOX Live when necessary.
	if !authResult.XBOXLiveAuthenticated && conn.authEnabled {
		_ = conn.WritePacket(&packet.Disconnect{Message: text.Colourf("<red>You must be logged in with XBOX Live to join.</red>")})
		return fmt.Errorf("connection %v was not authenticated to XBOX Live", conn.RemoteAddr())
	}
	// Make sure protocol numbers match.
	if pk.ClientProtocol != protocol.CurrentProtocol {
		// By default we assume the client is outdated.
		status := packet.PlayStatusLoginFailedClient
		if pk.ClientProtocol > protocol.CurrentProtocol {
			// The server is outdated in this case, so we have to change the status we send.
			status = packet.PlayStatusLoginFailedServer
		}
		_ = conn.WritePacket(&packet.PlayStatus{Status: status})
		return fmt.Errorf("%v connected with an incompatible protocol: expected protocol = %v, client protocol = %v", conn.identityData.DisplayName, protocol.CurrentProtocol, pk.ClientProtocol)
	}
	if err := conn.enableEncryption(authResult.PublicKey); err != nil {
		return fmt.Errorf("error enabling encryption: %v", err)
	}
	return nil
}

// handleClientToServerHandshake handles an incoming ClientToServerHandshake packet.
func (conn *Conn) handleClientToServerHandshake() error {
	// The next expected packet is a resource pack client response.
	conn.expect(packet.IDResourcePackClientResponse, packet.IDClientCacheStatus)
	if err := conn.WritePacket(&packet.NetworkSettings{CompressionThreshold: 512}); err != nil {
		return fmt.Errorf("error sending network settings: %v", err)
	}
	if err := conn.WritePacket(&packet.PlayStatus{Status: packet.PlayStatusLoginSuccess}); err != nil {
		return fmt.Errorf("error sending play status login success: %v", err)
	}
	pk := &packet.ResourcePacksInfo{TexturePackRequired: conn.texturePacksRequired}
	for _, pack := range conn.resourcePacks {
		// If it has behaviours, add it to the behaviour pack list. If not, we add it to the texture packs
		// list.
		if pack.HasBehaviours() {
			behaviourPack := protocol.BehaviourPackInfo{UUID: pack.UUID(), Version: pack.Version(), Size: uint64(pack.Len())}
			if pack.HasScripts() {
				// One of the resource packs has scripts, so we set HasScripts in the packet to true.
				pk.HasScripts = true
				behaviourPack.HasScripts = true
			}
			pk.BehaviourPacks = append(pk.BehaviourPacks, behaviourPack)
			continue
		}
		texturePack := protocol.TexturePackInfo{UUID: pack.UUID(), Version: pack.Version(), Size: uint64(pack.Len())}
		pk.TexturePacks = append(pk.TexturePacks, texturePack)
	}
	// Finally we send the packet after the play status.
	if err := conn.WritePacket(pk); err != nil {
		return fmt.Errorf("error sending resource packs info: %v", err)
	}
	return nil
}

// saltClaims holds the claims for the salt sent by the server in the ServerToClientHandshake packet.
type saltClaims struct {
	Salt string `json:"salt"`
}

// handleServerToClientHandshake handles an incoming ServerToClientHandshake packet. It initialises encryption
// on the client side of the connection, using the hash and the public key from the server exposed in the
// packet.
func (conn *Conn) handleServerToClientHandshake(pk *packet.ServerToClientHandshake) error {
	tok, err := jwt.ParseSigned(string(pk.JWT))
	if err != nil {
		return fmt.Errorf("parse server token: %w", err)
	}
	//lint:ignore S1005 Double assignment is done explicitly to prevent panics.
	raw, _ := tok.Headers[0].ExtraHeaders["x5u"]
	kStr, _ := raw.(string)

	pub := new(ecdsa.PublicKey)
	if err := login.ParsePublicKey(kStr, pub); err != nil {
		return fmt.Errorf("parse server public key: %w", err)
	}

	var c saltClaims
	if err := tok.Claims(pub, &c); err != nil {
		return fmt.Errorf("verify claims: %w", err)
	}
	c.Salt = strings.TrimRight(c.Salt, "=")
	salt, err := base64.RawStdEncoding.DecodeString(c.Salt)
	if err != nil {
		return fmt.Errorf("error base64 decoding ServerToClientHandshake salt: %v", err)
	}

	x, _ := pub.Curve.ScalarMult(pub.X, pub.Y, conn.privateKey.D.Bytes())
	// Make sure to pad the shared secret up to 96 bytes.
	sharedSecret := append(bytes.Repeat([]byte{0}, 48-len(x.Bytes())), x.Bytes()...)

	keyBytes := sha256.Sum256(append(salt, sharedSecret...))

	// Finally we enable encryption for the enc and dec using the secret pubKey bytes we produced.
	conn.enc.EnableEncryption(keyBytes)
	conn.dec.EnableEncryption(keyBytes)

	// We write a ClientToServerHandshake packet (which has no payload) as a response.
	_ = conn.WritePacket(&packet.ClientToServerHandshake{})
	return nil
}

// handleClientCacheStatus handles a ClientCacheStatus packet sent by the client. It specifies if the client
// has support for the client blob cache.
func (conn *Conn) handleClientCacheStatus(pk *packet.ClientCacheStatus) error {
	conn.cacheEnabled = pk.Enabled
	return nil
}

// handleResourcePacksInfo handles a ResourcePacksInfo packet sent by the server. The client responds by
// sending the packs it needs downloaded.
func (conn *Conn) handleResourcePacksInfo(pk *packet.ResourcePacksInfo) error {
	// First create a new resource pack queue with the information in the packet so we can download them
	// properly later.
	conn.packQueue = &resourcePackQueue{
		packAmount:       len(pk.TexturePacks) + len(pk.BehaviourPacks),
		downloadingPacks: make(map[string]downloadingPack),
		awaitingPacks:    make(map[string]*downloadingPack),
	}
	packsToDownload := make([]string, 0, len(pk.TexturePacks)+len(pk.BehaviourPacks))

	for _, pack := range pk.TexturePacks {
		if _, ok := conn.packQueue.downloadingPacks[pack.UUID]; ok {
			conn.log.Printf("duplicate texture pack entry %v in resource pack info\n", pack.UUID)
			conn.packQueue.packAmount--
			continue
		}
		// This UUID_Version is a hack Mojang put in place.
		packsToDownload = append(packsToDownload, pack.UUID+"_"+pack.Version)
		conn.packQueue.downloadingPacks[pack.UUID] = downloadingPack{size: pack.Size, buf: bytes.NewBuffer(make([]byte, 0, pack.Size)), newFrag: make(chan []byte)}
	}
	for _, pack := range pk.BehaviourPacks {
		if _, ok := conn.packQueue.downloadingPacks[pack.UUID]; ok {
			conn.log.Printf("duplicate behaviour pack entry %v in resource pack info\n", pack.UUID)
			conn.packQueue.packAmount--
			continue
		}
		// This UUID_Version is a hack Mojang put in place.
		packsToDownload = append(packsToDownload, pack.UUID+"_"+pack.Version)
		conn.packQueue.downloadingPacks[pack.UUID] = downloadingPack{size: pack.Size, buf: bytes.NewBuffer(make([]byte, 0, pack.Size)), newFrag: make(chan []byte)}
	}

	if len(packsToDownload) != 0 {
		conn.expect(packet.IDResourcePackDataInfo, packet.IDResourcePackChunkData)
		_ = conn.WritePacket(&packet.ResourcePackClientResponse{
			Response:        packet.PackResponseSendPacks,
			PacksToDownload: packsToDownload,
		})
		return nil
	}
	conn.expect(packet.IDResourcePackStack)

	_ = conn.WritePacket(&packet.ResourcePackClientResponse{Response: packet.PackResponseAllPacksDownloaded})
	return nil
}

// handleResourcePackStack handles a ResourcePackStack packet sent by the server. The stack defines the order
// that resource packs are applied in.
func (conn *Conn) handleResourcePackStack(pk *packet.ResourcePackStack) error {
	// We currently don't apply resource packs in any way, so instead we just check if all resource packs in
	// the stacks are also downloaded.
	for _, pack := range pk.TexturePacks {
		for i, behaviourPack := range pk.BehaviourPacks {
			if pack.UUID == behaviourPack.UUID {
				// We had a behaviour pack with the same UUID as the texture pack, so we drop the texture
				// pack and log it.
				conn.log.Printf("dropping behaviour pack with UUID %v due to a texture pack with the same UUID\n", pack.UUID)
				pk.BehaviourPacks = append(pk.BehaviourPacks[:i], pk.BehaviourPacks[i+1:]...)
			}
		}
		if !conn.hasPack(pack.UUID, pack.Version, false) {
			return fmt.Errorf("texture pack {uuid=%v, version=%v} not downloaded", pack.UUID, pack.Version)
		}
	}
	for _, pack := range pk.BehaviourPacks {
		if !conn.hasPack(pack.UUID, pack.Version, true) {
			return fmt.Errorf("behaviour pack {uuid=%v, version=%v} not downloaded", pack.UUID, pack.Version)
		}
	}
	conn.expect(packet.IDStartGame)
	_ = conn.WritePacket(&packet.ResourcePackClientResponse{Response: packet.PackResponseCompleted})
	return nil
}

// hasPack checks if the connection has a resource pack downloaded with the UUID and version passed, provided
// the pack either has or does not have behaviours in it.
func (conn *Conn) hasPack(uuid string, version string, hasBehaviours bool) bool {
	for _, exempted := range exemptedPacks {
		if exempted.uuid == uuid && exempted.version == version {
			// The server may send this resource pack on the stack without sending it in the info, as the client
			// always has it downloaded.
			return true
		}
	}
	conn.packMu.Lock()
	defer conn.packMu.Unlock()

	for _, pack := range conn.resourcePacks {
		if pack.UUID() == uuid && pack.Version() == version && pack.HasBehaviours() == hasBehaviours {
			return true
		}
	}
	return false
}

// packChunkSize is the size of a single chunk of data from a resource pack: 512 kB or 0.5 MB
const packChunkSize = 1024 * 128

// handleResourcePackClientResponse handles an incoming resource pack client response packet. The packet is
// handled differently depending on the response.
func (conn *Conn) handleResourcePackClientResponse(pk *packet.ResourcePackClientResponse) error {
	switch pk.Response {
	case packet.PackResponseRefused:
		// Even though this response is never sent, we handle it appropriately in case it is changed to work
		// correctly again.
		return conn.Close()
	case packet.PackResponseSendPacks:
		packs := pk.PacksToDownload
		conn.packQueue = &resourcePackQueue{packs: conn.resourcePacks}
		if err := conn.packQueue.Request(packs); err != nil {
			return fmt.Errorf("error looking up resource packs to download: %v", err)
		}
		// Proceed with the first resource pack download. We run all downloads in sequence rather than in
		// parallel, as it's less prone to packet loss.
		if err := conn.nextResourcePackDownload(); err != nil {
			return err
		}
	case packet.PackResponseAllPacksDownloaded:
		pk := &packet.ResourcePackStack{BaseGameVersion: protocol.CurrentVersion}
		for _, pack := range conn.resourcePacks {
			resourcePack := protocol.StackResourcePack{UUID: pack.UUID(), Version: pack.Version()}
			// If it has behaviours, add it to the behaviour pack list. If not, we add it to the texture packs
			// list.
			if pack.HasBehaviours() {
				pk.BehaviourPacks = append(pk.BehaviourPacks, resourcePack)
				continue
			}
			pk.TexturePacks = append(pk.TexturePacks, resourcePack)
		}
		for _, exempted := range exemptedPacks {
			pk.TexturePacks = append(pk.TexturePacks, protocol.StackResourcePack{
				UUID:    exempted.uuid,
				Version: exempted.version,
			})
		}
		if err := conn.WritePacket(pk); err != nil {
			return fmt.Errorf("error writing resource pack stack packet: %v", err)
		}
	case packet.PackResponseCompleted:
		conn.loggedIn = true
	default:
		return fmt.Errorf("unknown resource pack client response: %v", pk.Response)
	}
	return nil
}

// startGame sends a StartGame packet using the game data of the connection.
func (conn *Conn) startGame() {
	data := conn.gameData
	_ = conn.WritePacket(&packet.StartGame{
		Difficulty:                   data.Difficulty,
		EntityUniqueID:               data.EntityUniqueID,
		EntityRuntimeID:              data.EntityRuntimeID,
		PlayerGameMode:               data.PlayerGameMode,
		PlayerPosition:               data.PlayerPosition,
		Pitch:                        data.Pitch,
		Yaw:                          data.Yaw,
		Dimension:                    data.Dimension,
		WorldSpawn:                   data.WorldSpawn,
		GameRules:                    data.GameRules,
		Time:                         data.Time,
		Blocks:                       data.CustomBlocks,
		Items:                        data.Items,
		AchievementsDisabled:         true,
		Generator:                    1,
		EducationFeaturesEnabled:     true,
		MultiPlayerGame:              true,
		MultiPlayerCorrelationID:     uuid.Must(uuid.NewRandom()).String(),
		CommandsEnabled:              true,
		WorldName:                    data.WorldName,
		LANBroadcastEnabled:          true,
		PlayerMovementSettings:       data.PlayerMovementSettings,
		WorldGameMode:                data.WorldGameMode,
		ServerAuthoritativeInventory: data.ServerAuthoritativeInventory,
		Experiments:                  data.Experiments,
		GameVersion:                  protocol.CurrentVersion,
	})
	conn.expect(packet.IDRequestChunkRadius, packet.IDSetLocalPlayerAsInitialised)
}

// nextResourcePackDownload moves to the next resource pack to download and sends a resource pack data info
// packet with information about it.
func (conn *Conn) nextResourcePackDownload() error {
	pk, ok := conn.packQueue.NextPack()
	if !ok {
		return fmt.Errorf("no resource packs to download")
	}
	if err := conn.WritePacket(pk); err != nil {
		return fmt.Errorf("error sending resource pack data info packet: %v", err)
	}
	// Set the next expected packet to ResourcePackChunkRequest packets.
	conn.expect(packet.IDResourcePackChunkRequest)
	return nil
}

// handleResourcePackDataInfo handles a resource pack data info packet, which initiates the downloading of the
// pack by the client.
func (conn *Conn) handleResourcePackDataInfo(pk *packet.ResourcePackDataInfo) error {
	id := strings.Split(pk.UUID, "_")[0]

	pack, ok := conn.packQueue.downloadingPacks[id]
	if !ok {
		// We either already downloaded the pack or we got sent an invalid UUID, that did not match any pack
		// sent in the ResourcePacksInfo packet.
		return fmt.Errorf("unknown pack to download with UUID %v", id)
	}
	if pack.size != pk.Size {
		// Size mismatch: The ResourcePacksInfo packet had a size for the pack that did not match with the
		// size sent here.
		conn.log.Printf("pack %v had a different size in the ResourcePacksInfo packet than the ResourcePackDataInfo packet\n", id)
		pack.size = pk.Size
	}

	// Remove the resource pack from the downloading packs and add it to the awaiting packets.
	delete(conn.packQueue.downloadingPacks, id)
	conn.packQueue.awaitingPacks[id] = &pack

	pack.chunkSize = pk.DataChunkSize

	// The client calculates the chunk count by itself: You could in theory send a chunk count of 0 even
	// though there's data, and the client will still download normally.
	chunkCount := uint32(pk.Size / uint64(pk.DataChunkSize))
	if pk.Size%uint64(pk.DataChunkSize) != 0 {
		chunkCount++
	}

	idCopy := pk.UUID
	go func() {
		for i := uint32(0); i < chunkCount; i++ {
			_ = conn.WritePacket(&packet.ResourcePackChunkRequest{
				UUID:       idCopy,
				ChunkIndex: i,
			})
			select {
			case <-conn.close:
				return
			case frag := <-pack.newFrag:
				// Write the fragment to the full buffer of the downloading resource pack.
				_, _ = pack.buf.Write(frag)
			}
		}
		conn.packMu.Lock()
		defer conn.packMu.Unlock()

		if pack.buf.Len() != int(pack.size) {
			conn.log.Printf("incorrect resource pack size: expected %v, but got %v\n", pack.size, pack.buf.Len())
			return
		}
		// First parse the resource pack from the total byte buffer we obtained.
		pack, err := resource.FromBytes(pack.buf.Bytes())
		if err != nil {
			conn.log.Printf("invalid full resource pack data for UUID %v: %v\n", id, err)
			return
		}
		conn.packQueue.packAmount--
		// Finally we add the resource to the resource packs slice.
		conn.resourcePacks = append(conn.resourcePacks, pack)
		if conn.packQueue.packAmount == 0 {
			conn.expect(packet.IDResourcePackStack)
			_ = conn.WritePacket(&packet.ResourcePackClientResponse{Response: packet.PackResponseAllPacksDownloaded})
		}
	}()
	return nil
}

// handleResourcePackChunkData handles a resource pack chunk data packet, which holds a fragment of a resource
// pack that is being downloaded.
func (conn *Conn) handleResourcePackChunkData(pk *packet.ResourcePackChunkData) error {
	pk.UUID = strings.Split(pk.UUID, "_")[0]
	pack, ok := conn.packQueue.awaitingPacks[pk.UUID]
	if !ok {
		// We haven't received a ResourcePackDataInfo packet from the server, so we can't use this data to
		// download a resource pack.
		return fmt.Errorf("resource pack chunk data for resource pack that was not being downloaded")
	}
	lastData := pack.buf.Len()+int(pack.chunkSize) >= int(pack.size)
	if !lastData && uint32(len(pk.Data)) != pack.chunkSize {
		// The chunk data didn't have the full size and wasn't the last data to be sent for the resource pack,
		// meaning we got too little data.
		return fmt.Errorf("resource pack chunk data had a length of %v, but expected %v", len(pk.Data), pack.chunkSize)
	}
	if pk.ChunkIndex != pack.expectedIndex {
		return fmt.Errorf("resource pack chunk data had chunk index %v, but expected %v", pk.ChunkIndex, pack.expectedIndex)
	}
	pack.expectedIndex++
	pack.newFrag <- pk.Data
	return nil
}

// handleResourcePackChunkRequest handles a resource pack chunk request, which requests a part of the resource
// pack to be downloaded.
func (conn *Conn) handleResourcePackChunkRequest(pk *packet.ResourcePackChunkRequest) error {
	current := conn.packQueue.currentPack
	if current.UUID() != pk.UUID {
		return fmt.Errorf("resource pack chunk request had unexpected UUID: expected %v, but got %v", current.UUID(), pk.UUID)
	}
	if conn.packQueue.currentOffset != uint64(pk.ChunkIndex)*packChunkSize {
		return fmt.Errorf("resource pack chunk request had unexpected chunk index: expected %v, but got %v", conn.packQueue.currentOffset/packChunkSize, pk.ChunkIndex)
	}
	response := &packet.ResourcePackChunkData{
		UUID:       pk.UUID,
		ChunkIndex: pk.ChunkIndex,
		DataOffset: conn.packQueue.currentOffset,
		Data:       make([]byte, packChunkSize),
	}
	conn.packQueue.currentOffset += packChunkSize
	// We read the data directly into the response's data.
	if n, err := current.ReadAt(response.Data, int64(response.DataOffset)); err != nil {
		// If we hit an EOF, we don't need to return an error, as we've simply reached the end of the content
		// AKA the last chunk.
		if err != io.EOF {
			return fmt.Errorf("error reading resource pack chunk: %v", err)
		}
		response.Data = response.Data[:n]

		defer func() {
			if !conn.packQueue.AllDownloaded() {
				_ = conn.nextResourcePackDownload()
			} else {
				conn.expect(packet.IDResourcePackClientResponse)
			}
		}()
	}
	if err := conn.WritePacket(response); err != nil {
		return fmt.Errorf("error writing resource pack chunk data packet: %v", err)
	}

	return nil
}

// handleStartGame handles an incoming StartGame packet. It is the signal that the player has been added to a
// world, and it obtains most of its dedicated properties.
func (conn *Conn) handleStartGame(pk *packet.StartGame) error {
	conn.gameDataReceived.Store(true)
	conn.gameData = GameData{
		Difficulty:                   pk.Difficulty,
		WorldName:                    pk.WorldName,
		EntityUniqueID:               pk.EntityUniqueID,
		EntityRuntimeID:              pk.EntityRuntimeID,
		PlayerGameMode:               pk.PlayerGameMode,
		PlayerPosition:               pk.PlayerPosition,
		Pitch:                        pk.Pitch,
		Yaw:                          pk.Yaw,
		Dimension:                    pk.Dimension,
		WorldSpawn:                   pk.WorldSpawn,
		GameRules:                    pk.GameRules,
		Time:                         pk.Time,
		CustomBlocks:                 pk.Blocks,
		Items:                        pk.Items,
		PlayerMovementSettings:       pk.PlayerMovementSettings,
		WorldGameMode:                pk.WorldGameMode,
		ServerAuthoritativeInventory: pk.ServerAuthoritativeInventory,
		Experiments:                  pk.Experiments,
	}
	for _, item := range pk.Items {
		if item.Name == "minecraft:shield" {
			conn.shieldID.Store(int32(item.RuntimeID))
		}
	}

	conn.loggedIn = true

	conn.expect(packet.IDChunkRadiusUpdated, packet.IDPlayStatus)
	_ = conn.WritePacket(&packet.RequestChunkRadius{ChunkRadius: int32(conn.chunkRadius)})
	return nil
}

// handleRequestChunkRadius handles an incoming RequestChunkRadius packet. It sets the initial chunk radius
// of the connection, and spawns the player.
func (conn *Conn) handleRequestChunkRadius(pk *packet.RequestChunkRadius) error {
	if pk.ChunkRadius < 1 {
		return fmt.Errorf("requested chunk radius must be at least 1, got %v", pk.ChunkRadius)
	}
	conn.expect(packet.IDSetLocalPlayerAsInitialised)
	_ = conn.WritePacket(&packet.ChunkRadiusUpdated{ChunkRadius: pk.ChunkRadius})
	conn.chunkRadius = int(pk.ChunkRadius)

	// The client crashes when not sending all biomes, due to achievements assuming all biomes are present.
	//noinspection SpellCheckingInspection
	const s = `CgAKDWJhbWJvb19qdW5nbGUFCGRvd25mYWxsZmZmPwULdGVtcGVyYXR1cmUzM3M/AAoTYmFtYm9vX2p1bmdsZV9oaWxscwUIZG93bmZhbGxmZmY/BQt0ZW1wZXJhdHVyZTMzcz8ACgViZWFjaAUIZG93bmZhbGzNzMw+BQt0ZW1wZXJhdHVyZc3MTD8ACgxiaXJjaF9mb3Jlc3QFCGRvd25mYWxsmpkZPwULdGVtcGVyYXR1cmWamRk/AAoSYmlyY2hfZm9yZXN0X2hpbGxzBQhkb3duZmFsbJqZGT8FC3RlbXBlcmF0dXJlmpkZPwAKGmJpcmNoX2ZvcmVzdF9oaWxsc19tdXRhdGVkBQhkb3duZmFsbM3MTD8FC3RlbXBlcmF0dXJlMzMzPwAKFGJpcmNoX2ZvcmVzdF9tdXRhdGVkBQhkb3duZmFsbM3MTD8FC3RlbXBlcmF0dXJlMzMzPwAKCmNvbGRfYmVhY2gFCGRvd25mYWxsmpmZPgULdGVtcGVyYXR1cmXNzEw9AAoKY29sZF9vY2VhbgUIZG93bmZhbGwAAAA/BQt0ZW1wZXJhdHVyZQAAAD8ACgpjb2xkX3RhaWdhBQhkb3duZmFsbM3MzD4FC3RlbXBlcmF0dXJlAAAAvwAKEGNvbGRfdGFpZ2FfaGlsbHMFCGRvd25mYWxszczMPgULdGVtcGVyYXR1cmUAAAC/AAoSY29sZF90YWlnYV9tdXRhdGVkBQhkb3duZmFsbM3MzD4FC3RlbXBlcmF0dXJlAAAAvwAKD2RlZXBfY29sZF9vY2VhbgUIZG93bmZhbGwAAAA/BQt0ZW1wZXJhdHVyZQAAAD8AChFkZWVwX2Zyb3plbl9vY2VhbgUIZG93bmZhbGwAAAA/BQt0ZW1wZXJhdHVyZQAAAAAAChNkZWVwX2x1a2V3YXJtX29jZWFuBQhkb3duZmFsbAAAAD8FC3RlbXBlcmF0dXJlAAAAPwAKCmRlZXBfb2NlYW4FCGRvd25mYWxsAAAAPwULdGVtcGVyYXR1cmUAAAA/AAoPZGVlcF93YXJtX29jZWFuBQhkb3duZmFsbAAAAD8FC3RlbXBlcmF0dXJlAAAAPwAKBmRlc2VydAUIZG93bmZhbGwAAAAABQt0ZW1wZXJhdHVyZQAAAEAACgxkZXNlcnRfaGlsbHMFCGRvd25mYWxsAAAAAAULdGVtcGVyYXR1cmUAAABAAAoOZGVzZXJ0X211dGF0ZWQFCGRvd25mYWxsAAAAAAULdGVtcGVyYXR1cmUAAABAAAoNZXh0cmVtZV9oaWxscwUIZG93bmZhbGyamZk+BQt0ZW1wZXJhdHVyZc3MTD4AChJleHRyZW1lX2hpbGxzX2VkZ2UFCGRvd25mYWxsmpmZPgULdGVtcGVyYXR1cmXNzEw+AAoVZXh0cmVtZV9oaWxsc19tdXRhdGVkBQhkb3duZmFsbJqZmT4FC3RlbXBlcmF0dXJlzcxMPgAKGGV4dHJlbWVfaGlsbHNfcGx1c190cmVlcwUIZG93bmZhbGyamZk+BQt0ZW1wZXJhdHVyZc3MTD4ACiBleHRyZW1lX2hpbGxzX3BsdXNfdHJlZXNfbXV0YXRlZAUIZG93bmZhbGyamZk+BQt0ZW1wZXJhdHVyZc3MTD4ACg1mbG93ZXJfZm9yZXN0BQhkb3duZmFsbM3MTD8FC3RlbXBlcmF0dXJlMzMzPwAKBmZvcmVzdAUIZG93bmZhbGzNzEw/BQt0ZW1wZXJhdHVyZTMzMz8ACgxmb3Jlc3RfaGlsbHMFCGRvd25mYWxszcxMPwULdGVtcGVyYXR1cmUzMzM/AAoMZnJvemVuX29jZWFuBQhkb3duZmFsbAAAAD8FC3RlbXBlcmF0dXJlAAAAAAAKDGZyb3plbl9yaXZlcgUIZG93bmZhbGwAAAA/BQt0ZW1wZXJhdHVyZQAAAAAACgRoZWxsBQhkb3duZmFsbAAAAAAFC3RlbXBlcmF0dXJlAAAAQAAKDWljZV9tb3VudGFpbnMFCGRvd25mYWxsAAAAPwULdGVtcGVyYXR1cmUAAAAAAAoKaWNlX3BsYWlucwUIZG93bmZhbGwAAAA/BQt0ZW1wZXJhdHVyZQAAAAAAChFpY2VfcGxhaW5zX3NwaWtlcwUIZG93bmZhbGwAAIA/BQt0ZW1wZXJhdHVyZQAAAAAACgZqdW5nbGUFCGRvd25mYWxsZmZmPwULdGVtcGVyYXR1cmUzM3M/AAoLanVuZ2xlX2VkZ2UFCGRvd25mYWxszcxMPwULdGVtcGVyYXR1cmUzM3M/AAoTanVuZ2xlX2VkZ2VfbXV0YXRlZAUIZG93bmZhbGzNzEw/BQt0ZW1wZXJhdHVyZTMzcz8ACgxqdW5nbGVfaGlsbHMFCGRvd25mYWxsZmZmPwULdGVtcGVyYXR1cmUzM3M/AAoOanVuZ2xlX211dGF0ZWQFCGRvd25mYWxsZmZmPwULdGVtcGVyYXR1cmUzM3M/AAoTbGVnYWN5X2Zyb3plbl9vY2VhbgUIZG93bmZhbGwAAAA/BQt0ZW1wZXJhdHVyZQAAAAAACg5sdWtld2FybV9vY2VhbgUIZG93bmZhbGwAAAA/BQt0ZW1wZXJhdHVyZQAAAD8ACgptZWdhX3RhaWdhBQhkb3duZmFsbM3MTD8FC3RlbXBlcmF0dXJlmpmZPgAKEG1lZ2FfdGFpZ2FfaGlsbHMFCGRvd25mYWxszcxMPwULdGVtcGVyYXR1cmWamZk+AAoEbWVzYQUIZG93bmZhbGwAAAAABQt0ZW1wZXJhdHVyZQAAAEAACgptZXNhX2JyeWNlBQhkb3duZmFsbAAAAAAFC3RlbXBlcmF0dXJlAAAAQAAKDG1lc2FfcGxhdGVhdQUIZG93bmZhbGwAAAAABQt0ZW1wZXJhdHVyZQAAAEAAChRtZXNhX3BsYXRlYXVfbXV0YXRlZAUIZG93bmZhbGwAAAAABQt0ZW1wZXJhdHVyZQAAAEAAChJtZXNhX3BsYXRlYXVfc3RvbmUFCGRvd25mYWxsAAAAAAULdGVtcGVyYXR1cmUAAABAAAoabWVzYV9wbGF0ZWF1X3N0b25lX211dGF0ZWQFCGRvd25mYWxsAAAAAAULdGVtcGVyYXR1cmUAAABAAAoPbXVzaHJvb21faXNsYW5kBQhkb3duZmFsbAAAgD8FC3RlbXBlcmF0dXJlZmZmPwAKFW11c2hyb29tX2lzbGFuZF9zaG9yZQUIZG93bmZhbGwAAIA/BQt0ZW1wZXJhdHVyZWZmZj8ACgVvY2VhbgUIZG93bmZhbGwAAAA/BQt0ZW1wZXJhdHVyZQAAAD8ACgZwbGFpbnMFCGRvd25mYWxszczMPgULdGVtcGVyYXR1cmXNzEw/AAobcmVkd29vZF90YWlnYV9oaWxsc19tdXRhdGVkBQhkb3duZmFsbM3MTD8FC3RlbXBlcmF0dXJlmpmZPgAKFXJlZHdvb2RfdGFpZ2FfbXV0YXRlZAUIZG93bmZhbGzNzEw/BQt0ZW1wZXJhdHVyZQAAgD4ACgVyaXZlcgUIZG93bmZhbGwAAAA/BQt0ZW1wZXJhdHVyZQAAAD8ACg1yb29mZWRfZm9yZXN0BQhkb3duZmFsbM3MTD8FC3RlbXBlcmF0dXJlMzMzPwAKFXJvb2ZlZF9mb3Jlc3RfbXV0YXRlZAUIZG93bmZhbGzNzEw/BQt0ZW1wZXJhdHVyZTMzMz8ACgdzYXZhbm5hBQhkb3duZmFsbAAAAAAFC3RlbXBlcmF0dXJlmpmZPwAKD3NhdmFubmFfbXV0YXRlZAUIZG93bmZhbGwAAAA/BQt0ZW1wZXJhdHVyZc3MjD8ACg9zYXZhbm5hX3BsYXRlYXUFCGRvd25mYWxsAAAAAAULdGVtcGVyYXR1cmUAAIA/AAoXc2F2YW5uYV9wbGF0ZWF1X211dGF0ZWQFCGRvd25mYWxsAAAAPwULdGVtcGVyYXR1cmUAAIA/AAoLc3RvbmVfYmVhY2gFCGRvd25mYWxsmpmZPgULdGVtcGVyYXR1cmXNzEw+AAoQc3VuZmxvd2VyX3BsYWlucwUIZG93bmZhbGzNzMw+BQt0ZW1wZXJhdHVyZc3MTD8ACglzd2FtcGxhbmQFCGRvd25mYWxsAAAAPwULdGVtcGVyYXR1cmXNzEw/AAoRc3dhbXBsYW5kX211dGF0ZWQFCGRvd25mYWxsAAAAPwULdGVtcGVyYXR1cmXNzEw/AAoFdGFpZ2EFCGRvd25mYWxszcxMPwULdGVtcGVyYXR1cmUAAIA+AAoLdGFpZ2FfaGlsbHMFCGRvd25mYWxszcxMPwULdGVtcGVyYXR1cmUAAIA+AAoNdGFpZ2FfbXV0YXRlZAUIZG93bmZhbGzNzEw/BQt0ZW1wZXJhdHVyZQAAgD4ACgd0aGVfZW5kBQhkb3duZmFsbAAAAD8FC3RlbXBlcmF0dXJlAAAAPwAKCndhcm1fb2NlYW4FCGRvd25mYWxsAAAAPwULdGVtcGVyYXR1cmUAAAA/AAA=`
	b, _ := base64.StdEncoding.DecodeString(s)
	_ = conn.WritePacket(&packet.BiomeDefinitionList{
		SerialisedBiomeDefinitions: b,
	})

	_ = conn.WritePacket(&packet.PlayStatus{Status: packet.PlayStatusPlayerSpawn})

	_ = conn.WritePacket(&packet.CreativeContent{})
	return nil
}

// handleChunkRadiusUpdated handles an incoming ChunkRadiusUpdated packet, which updates the initial chunk
// radius of the connection.
func (conn *Conn) handleChunkRadiusUpdated(pk *packet.ChunkRadiusUpdated) error {
	if pk.ChunkRadius < 1 {
		return fmt.Errorf("new chunk radius must be at least 1, got %v", pk.ChunkRadius)
	}
	conn.expect(packet.IDPlayStatus)
	conn.chunkRadius = int(pk.ChunkRadius)
	return nil
}

// handleSetLocalPlayerAsInitialised handles an incoming SetLocalPlayerAsInitialised packet. It is the final
// packet in the spawning sequence and it marks the point where a server sided connection is considered
// logged in.
func (conn *Conn) handleSetLocalPlayerAsInitialised(pk *packet.SetLocalPlayerAsInitialised) error {
	if pk.EntityRuntimeID != conn.gameData.EntityRuntimeID {
		return fmt.Errorf("entity runtime ID mismatch: entity runtime ID in StartGame and SetLocalPlayerAsInitialised packets should be equal")
	}
	if conn.waitingForSpawn.CAS(true, false) {
		close(conn.spawn)
	}
	return nil
}

// handlePlayStatus handles an incoming PlayStatus packet. It reacts differently depending on the status
// found in the packet.
func (conn *Conn) handlePlayStatus(pk *packet.PlayStatus) error {
	switch pk.Status {
	case packet.PlayStatusLoginSuccess:
		if err := conn.WritePacket(&packet.ClientCacheStatus{Enabled: conn.cacheEnabled}); err != nil {
			return fmt.Errorf("error sending client cache status: %v", err)
		}
		// The next packet we expect is the ResourcePacksInfo packet.
		conn.expect(packet.IDResourcePacksInfo)
		return conn.Flush()
	case packet.PlayStatusLoginFailedClient:
		_ = conn.Close()
		return fmt.Errorf("client outdated")
	case packet.PlayStatusLoginFailedServer:
		_ = conn.Close()
		return fmt.Errorf("server outdated")
	case packet.PlayStatusPlayerSpawn:
		// We've spawned and can send the last packet in the spawn sequence.
		if conn.waitingForSpawn.CAS(true, false) {
			close(conn.spawn)
			_ = conn.WritePacket(&packet.SetLocalPlayerAsInitialised{EntityRuntimeID: conn.gameData.EntityRuntimeID})
		}
		return nil
	case packet.PlayStatusLoginFailedInvalidTenant:
		_ = conn.Close()
		return fmt.Errorf("invalid edu edition game owner")
	case packet.PlayStatusLoginFailedVanillaEdu:
		_ = conn.Close()
		return fmt.Errorf("cannot join an edu edition game on vanilla")
	case packet.PlayStatusLoginFailedEduVanilla:
		_ = conn.Close()
		return fmt.Errorf("cannot join a vanilla game on edu edition")
	case packet.PlayStatusLoginFailedServerFull:
		_ = conn.Close()
		return fmt.Errorf("server full")
	default:
		return fmt.Errorf("unknown play status in PlayStatus packet %v", pk.Status)
	}
}

// enableEncryption enables encryption on the server side over the connection. It sends an unencrypted
// handshake packet to the client and enables encryption after that.
func (conn *Conn) enableEncryption(clientPublicKey *ecdsa.PublicKey) error {
	signer, _ := jose.NewSigner(jose.SigningKey{Key: conn.privateKey, Algorithm: jose.ES384}, &jose.SignerOptions{
		ExtraHeaders: map[jose.HeaderKey]interface{}{"x5u": login.MarshalPublicKey(&conn.privateKey.PublicKey)},
	})
	// We produce an encoded JWT using the header and payload above, then we send the JWT in a ServerToClient-
	// Handshake packet so that the client can initialise encryption.
	serverJWT, err := jwt.Signed(signer).Claims(saltClaims{Salt: base64.RawStdEncoding.EncodeToString(conn.salt)}).CompactSerialize()
	if err != nil {
		return fmt.Errorf("compact serialise server JWT: %w", err)
	}
	if err := conn.WritePacket(&packet.ServerToClientHandshake{JWT: []byte(serverJWT)}); err != nil {
		return fmt.Errorf("error sending ServerToClientHandshake packet: %v", err)
	}
	// Flush immediately as we'll enable encryption after this.
	_ = conn.Flush()

	// We first compute the shared secret.
	x, _ := clientPublicKey.Curve.ScalarMult(clientPublicKey.X, clientPublicKey.Y, conn.privateKey.D.Bytes())

	sharedSecret := append(bytes.Repeat([]byte{0}, 48-len(x.Bytes())), x.Bytes()...)

	keyBytes := sha256.Sum256(append(conn.salt, sharedSecret...))

	// Finally we enable encryption for the encoder and decoder using the secret key bytes we produced.
	conn.enc.EnableEncryption(keyBytes)
	conn.dec.EnableEncryption(keyBytes)

	return nil
}

// expect sets the packet IDs that are next expected to arrive.
func (conn *Conn) expect(packetIDs ...uint32) {
	conn.expectedIDs.Store(packetIDs)
}

// closeErr returns an adequate connection closed error for the op passed. If the connection was closed
// through a Disconnect packet, the message is contained.
func (conn *Conn) closeErr(op string) error {
	if msg := conn.disconnectMessage.Load(); msg != "" {
		return conn.wrap(DisconnectError(msg), op)
	}
	return conn.wrap(errClosed, op)
}
